
/*
 +------------------------------------------------------------------------+
 | Phalcon Framework                                                      |
 +------------------------------------------------------------------------+
 | Copyright (c) 2011-2017 Phalcon Team (https://phalconphp.com)          |
 +------------------------------------------------------------------------+
 | This source file is subject to the New BSD License that is bundled     |
 | with this package in the file LICENSE.txt.                             |
 |                                                                        |
 | If you did not receive a copy of the license and are unable to         |
 | obtain it through the world-wide-web, please send an email             |
 | to license@phalconphp.com so we can send you a copy immediately.       |
 +------------------------------------------------------------------------+
 | Authors: Andres Gutierrez <andres@phalconphp.com>                      |
 |          Eduar Carvajal <eduar@phalconphp.com>                         |
 +------------------------------------------------------------------------+
 */

namespace Phalcon;

use Phalcon\DiInterface;
use Phalcon\Security\Random;
use Phalcon\Security\Exception;
use Phalcon\Di\InjectionAwareInterface;
use Phalcon\Session\AdapterInterface as SessionInterface;

/**
 * Phalcon\Security
 *
 * This component provides a set of functions to improve the security in Phalcon applications
 *
 *<code>
 * $login    = $this->request->getPost("login");
 * $password = $this->request->getPost("password");
 *
 * $user = Users::findFirstByLogin($login);
 *
 * if ($user) {
 *     if ($this->security->checkHash($password, $user->password)) {
 *         // The password is valid
 *     }
 * }
 *</code>
 */
class Security implements InjectionAwareInterface
{

	protected _dependencyInjector;

	protected _workFactor = 8 { set, get };

	protected _numberBytes = 16;

	protected _tokenKeySessionID = "$PHALCON/CSRF/KEY$";

	protected _tokenValueSessionID = "$PHALCON/CSRF$";

	protected _token;

	protected _tokenKey;

	protected _random;

	protected _defaultHash;

	const CRYPT_DEFAULT	   =	0;

	const CRYPT_STD_DES	   =	1;

	const CRYPT_EXT_DES	   =	2;

	const CRYPT_MD5		   =	3;

	const CRYPT_BLOWFISH       =	4;

	const CRYPT_BLOWFISH_A     =    5;

	const CRYPT_BLOWFISH_X     =	6;

	const CRYPT_BLOWFISH_Y     =	7;

	const CRYPT_SHA256	   =	8;

	const CRYPT_SHA512	   =	9;

	/**
	 * Phalcon\Security constructor
	 */
	public function __construct()
	{
		let this->_random = new Random();
	}

	/**
	 * Sets the dependency injector
	 */
	public function setDI(<DiInterface> dependencyInjector) -> void
	{
		let this->_dependencyInjector = dependencyInjector;
	}

	/**
	 * Returns the internal dependency injector
	 */
	public function getDI() -> <DiInterface>
	{
		return this->_dependencyInjector;
	}

	/**
	 * Sets a number of bytes to be generated by the openssl pseudo random generator
	 */
	public function setRandomBytes(long! randomBytes) -> <Security>
	{
		let this->_numberBytes = randomBytes;

		return this;
	}

	/**
	 * Returns a number of bytes to be generated by the openssl pseudo random generator
	 */
	public function getRandomBytes() -> string
	{
		return this->_numberBytes;
	}

	/**
	 * Returns a secure random number generator instance
	 */
	public function getRandom() -> <Random>
	{
		return this->_random;
	}

	/**
	 * Generate a >22-length pseudo random string to be used as salt for passwords
	 */
	public function getSaltBytes(int numberBytes = 0) -> string
	{
		var safeBytes;

		if !numberBytes {
			let numberBytes = (int) this->_numberBytes;
		}

		loop {
			let safeBytes = this->_random->base64Safe(numberBytes);

			if !safeBytes || strlen(safeBytes) < numberBytes {
				continue;
			}

			break;
		}

		return safeBytes;
	}

	/**
	 * Creates a password hash using bcrypt with a pseudo random salt
	 */
	public function hash(string password, int workFactor = 0) -> string
	{
		int hash;
		string variant;
		var saltBytes;

		if !workFactor {
			let workFactor = (int) this->_workFactor;
		}

		let hash = (int) this->_defaultHash;

		switch hash {

			case self::CRYPT_BLOWFISH_A:
				let variant = "a";
				break;

			case self::CRYPT_BLOWFISH_X:
				let variant = "x";
				break;

			case self::CRYPT_BLOWFISH_Y:
				let variant = "y";
				break;

			case self::CRYPT_MD5:
				let variant = "1";
				break;

			case self::CRYPT_SHA256:
				let variant = "5";
				break;

			case self::CRYPT_SHA512:
				let variant = "6";
				break;

			case self::CRYPT_DEFAULT:
			default:
				let variant = "y";
				break;
		}

		switch hash {

			case self::CRYPT_STD_DES:
			case self::CRYPT_EXT_DES:

				/* Standard DES-based hash with a two character salt from the alphabet "./0-9A-Za-z". */

				if (hash == self::CRYPT_EXT_DES) {
					let saltBytes = "_".this->getSaltBytes(8);
				} else {
					let saltBytes = this->getSaltBytes(2);
				}

				if typeof saltBytes != "string" {
					throw new Exception("Unable to get random bytes for the salt");
				}

				return crypt(password, saltBytes);

			case self::CRYPT_MD5:
			case self::CRYPT_SHA256:
			case self::CRYPT_SHA512:

				/*
				 * MD5 hashing with a twelve character salt
				 * SHA-256/SHA-512 hash with a sixteen character salt.
				 */

				let saltBytes = this->getSaltBytes(hash == self::CRYPT_MD5 ? 12 : 16);

				if typeof saltBytes != "string" {
					throw new Exception("Unable to get random bytes for the salt");
				}

				return crypt(password, "$" . variant . "$"  . saltBytes . "$");

			case self::CRYPT_DEFAULT:
			case self::CRYPT_BLOWFISH:
			case self::CRYPT_BLOWFISH_X:
			case self::CRYPT_BLOWFISH_Y:
			default:

				/*
				 * Blowfish hashing with a salt as follows: "$2a$", "$2x$" or "$2y$",
				 * a two digit cost parameter, "$", and 22 characters from the alphabet
				 * "./0-9A-Za-z". Using characters outside of this range in the salt
				 * will cause crypt() to return a zero-length string. The two digit cost
				 * parameter is the base-2 logarithm of the iteration count for the
				 * underlying Blowfish-based hashing algorithm and must be in
				 * range 04-31, values outside this range will cause crypt() to fail.
				 */

				let saltBytes = this->getSaltBytes(22);
				if typeof saltBytes != "string" {
					throw new Exception("Unable to get random bytes for the salt");
				}

				if workFactor < 4 {
					let workFactor = 4;
				} else {
					if workFactor > 31 {
						let workFactor = 31;
					}
				}

				return crypt(password, "$2" . variant . "$" . sprintf("%02s", workFactor) . "$" . saltBytes . "$");
		}

		return "";
	}

	/**
	 * Checks a plain text password and its hash version to check if the password matches
	 */
	public function checkHash(string password, string passwordHash, int maxPassLength = 0) -> boolean
	{
		char ch;
		string cryptedHash;
		int i, sum, cryptedLength, passwordLength;

		if maxPassLength {
			if maxPassLength > 0 && strlen(password) > maxPassLength {
				return false;
			}
		}

		let cryptedHash = (string) crypt(password, passwordHash);

		let cryptedLength = strlen(cryptedHash),
			passwordLength = strlen(passwordHash);

		let cryptedHash .= passwordHash;

		let sum = cryptedLength - passwordLength;
		for i, ch in passwordHash {
			let sum = sum | (cryptedHash[i] ^ ch);
		}

		return 0 === sum;
	}

	/**
	 * Checks if a password hash is a valid bcrypt's hash
	 */
	public function isLegacyHash(string passwordHash) -> boolean
	{
		return starts_with(passwordHash, "$2a$");
	}

	/**
	 * Generates a pseudo random token key to be used as input's name in a CSRF check
	 */
	public function getTokenKey() -> string
	{
		var dependencyInjector, session;

		if null === this->_tokenKey {
			let dependencyInjector = <DiInterface> this->_dependencyInjector;
			if typeof dependencyInjector != "object" {
				throw new Exception("A dependency injection container is required to access the 'session' service");
			}

			let this->_tokenKey = this->_random->base64Safe(this->_numberBytes);
			let session = <SessionInterface> dependencyInjector->getShared("session");
			session->set(this->_tokenKeySessionID, this->_tokenKey);
		}

		return this->_tokenKey;
	}

	/**
	 * Generates a pseudo random token value to be used as input's value in a CSRF check
	 */
	public function getToken() -> string
	{
		var dependencyInjector, session;

		if null === this->_token {
			let this->_token = this->_random->base64Safe(this->_numberBytes);

			let dependencyInjector = <DiInterface> this->_dependencyInjector;

			if typeof dependencyInjector != "object" {
				throw new Exception("A dependency injection container is required to access the 'session' service");
			}

			let session = <SessionInterface> dependencyInjector->getShared("session");
			session->set(this->_tokenValueSessionID, this->_token);
		}

		return this->_token;
	}

	/**
	 * Check if the CSRF token sent in the request is the same that the current in session
	 */
	public function checkToken(var tokenKey = null, var tokenValue = null, boolean destroyIfValid = true) -> boolean
	{
		var dependencyInjector, session, request, equals, userToken, knownToken;

		let dependencyInjector = <DiInterface> this->_dependencyInjector;

		if typeof dependencyInjector != "object" {
			throw new Exception("A dependency injection container is required to access the 'session' service");
		}

		let session = <SessionInterface> dependencyInjector->getShared("session");

		if !tokenKey {
			let tokenKey = session->get(this->_tokenKeySessionID);
		}

		/**
		 * If tokenKey does not exist in session return false
		 */
		if !tokenKey {
			return false;
		}

		if !tokenValue {
			let request = dependencyInjector->getShared("request");

			/**
			 * We always check if the value is correct in post
			 */
			let userToken = request->getPost(tokenKey);
		} else {
			let userToken = tokenValue;
		}

		/**
		 * The value is the same?
		 */
		let knownToken = session->get(this->_tokenValueSessionID);
		let equals = hash_equals(knownToken, userToken);

		/**
		 * Remove the key and value of the CSRF token in session
		 */
		if equals && destroyIfValid {
			this->destroyToken();
		}

		return equals;
	}

	/**
	 * Returns the value of the CSRF token in session
	 */
	public function getSessionToken() -> string
	{
		var dependencyInjector, session;

		let dependencyInjector = <DiInterface> this->_dependencyInjector;

		if typeof dependencyInjector != "object" {
			throw new Exception("A dependency injection container is required to access the 'session' service");
		}

		let session = <SessionInterface> dependencyInjector->getShared("session");

		return session->get(this->_tokenValueSessionID);
	}

	/**
	 * Removes the value of the CSRF token and key from session
	 */
	public function destroyToken() -> <Security>
	{
		var dependencyInjector, session;

		let dependencyInjector = <DiInterface> this->_dependencyInjector;

		if typeof dependencyInjector != "object" {
			throw new Exception("A dependency injection container is required to access the 'session' service");
		}

		let session = <SessionInterface> dependencyInjector->getShared("session");

		session->remove(this->_tokenKeySessionID);
		session->remove(this->_tokenValueSessionID);

		let this->_token = null;
		let this->_tokenKey = null;

		return this;
	}

	/**
	 * Computes a HMAC
	 */
	public function computeHmac(string data, string key, string algo, boolean raw = false) -> string
	{
		var hmac;

		let hmac = hash_hmac(algo, data, key, raw);
		if !hmac {
			throw new Exception("Unknown hashing algorithm: %s" . algo);
		}

		return hmac;
	}

	/**
 	 * Sets the default hash
 	 */
	public function setDefaultHash(int defaultHash) -> <Security>
	{
		let this->_defaultHash = defaultHash;

		return this;
	}

	/**
 	 * Returns the default hash
 	 */
	public function getDefaultHash() -> int | null
	{
		return this->_defaultHash;
	}

	/**
	 * Testing for LibreSSL
	 */
	public function hasLibreSsl() -> boolean
	{
		if !defined("OPENSSL_VERSION_TEXT") {
			return false;
		}

		return strpos(OPENSSL_VERSION_TEXT, "LibreSSL") === 0;
	}

	/**
	 * Getting OpenSSL or LibreSSL version
	 *
	 * Parse OPENSSL_VERSION_TEXT because OPENSSL_VERSION_NUMBER is no use for LibreSSL.
	 * @link https://bugs.php.net/bug.php?id=71143
	 *
	 * <code>
	 * if ($security->getSslVersionNumber() >= 20105) {
	 *     // ...
	 * }
	 * </code>
	 */
	public function getSslVersionNumber() -> int
	{
		var matches;

		preg_match("#^(?:Libre|Open)SSL ([\d]+)\.([\d]+)(\.([\d]+))?$#", OPENSSL_VERSION_TEXT, matches);

		if !isset matches[2] {
			return 0;
		}

		var patch = 0;
		if isset matches[3] {
			let patch = intval(matches[3]);
		}

		return (10000 * intval(matches[2])) + (100 * intval(matches[2])) + patch;
	}
}
